package io.kljf.boot.modules.security.shiro;

import dm.jdbc.util.StringUtil;
import io.kljf.boot.common.constant.CommonConstant;
import io.kljf.boot.common.utils.JwtUtil;
import io.kljf.boot.common.utils.RedisUtil;
import io.kljf.boot.modules.security.service.IShiroService;
import io.kljf.boot.modules.security.shiro.JwtToken;
import io.kljf.boot.modules.sys.entity.UserEntity;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * @author katherine
 * @version 2020-11-30
 **/
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private IShiroService shiroService;
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 判断token是否支持
     *
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token != null && token instanceof JwtToken;
    }

    /**
     * 授权（验证权限时调用）
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserEntity user = (UserEntity) principalCollection.getPrimaryPrincipal();
        // 用户权限列表
        Set<String> permissions = shiroService.getUserPermissions(user);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permissions);
        return info;
    }

    /**
     * 认证（登陆时调用）
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String accessToken = (String) authenticationToken.getPrincipal();
        if (authenticationToken == null) {
            // 身份认证失败
            throw new AuthenticationException("token非法无效!");
        }
        // 校验token是否有效
        UserEntity loginUser = this.checkUserTokenIsEffect(accessToken);

        return new SimpleAuthenticationInfo(loginUser,accessToken,getName());
    }

    private UserEntity checkUserTokenIsEffect(String accessToken) {
        String username = JwtUtil.getUsername(accessToken);
        if (username == null) {
            throw new AuthenticationException("token非法无效！");
        }
        // 查询用户
        UserEntity loginUser = shiroService.getUserByUserName(username);
        if (loginUser == null) {
            throw new AuthenticationException("token非法无效！");
        }
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(accessToken, username, loginUser.getPassword())) {
            throw new AuthenticationException("Token失效，请重新登录!");
        }
        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 （实现： 用户在线操作不掉线功能）
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)，缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 注意： 前端请求Header中设置Authorization保持不变，校验有效性以缓存中的token为准。
     * 用户过期时间 = Jwt有效时间 * 2。
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (StringUtil.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.generateToken(userName, passWord);
                // 设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRES_IN * 2 / 1000);
            }
            return true;
        }
        return false;
    }

}